# SSN - map
# Copyright (c) 2009, jonoerik
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
# conditions are met:
#
#    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following
#	disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
#	disclaimer in the documentation and/or other materials provided with the distribution.
#    * The names of contributors may not be used to endorse or promote products derived from this software without specific
#	prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
# EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

#map
#contains map information, and map drawing code

#library imports
from sys import path as syspath
syspath.append('.//libtcod-1.4.1')
import libtcodpy as libtcod
import numpy

from util import *

from math import ceil

class ssn_map:
    '''a map of an area in ssn'''
    
    def __init__(self, g, w, h):
        '''initialise an empty map'''
        self.width = w
        self.height = h
        #create an array of tiles
        self.land = numpy.array([[ssn_tile('empty')]*self.width]*self.height)
        #set each tile to a unique tile instance
        for x,y in alltiles(self.width, self.height):
            self.land[y, x] = ssn_tile('empty')
        
        self.pc_start_pos = None

    def gen_city(self, g):
        #create the bsp root node
        city_bsp = libtcod.bsp_new_with_size(0, 0, self.width, self.height)
        #split the bsp tree according the the settings passed in bsp_settings
        libtcod.bsp_split_recursive(city_bsp, None, \
                                    g.const_bsp_recursion_levels, \
                                    g.const_bsp_min_w, g.const_bsp_min_h, \
                                    g.const_bsp_max_wh_ratio, g.const_bsp_max_hw_ratio)
        #a list of rect objects (from util.py) representing buildings
        buildings = []
        
        def add_building(node, other_data):
            '''used as a callback for the bsp tree
            adds leaf nodes to a list'''
            if libtcod.bsp_is_leaf(node):
                buildings.append(rect(node.x + g.const_building_border, \
                                      node.y + g.const_building_border, \
                                      node.w - g.const_building_border * 2, \
                                      node.h - g.const_building_border * 2))
            return True
            
        libtcod.bsp_traverse_level_order(city_bsp, add_building)
        libtcod.bsp_delete(city_bsp)
        
        #remove some buildings; create courtyards instead
        num_courtyards = int(ceil(len(buildings) / (g.const_areas_per_courtyard )))
        for i in xrange(num_courtyards):
            remove_index = rand(0, len(buildings) - 1)
            buildings = buildings[:remove_index] + buildings[remove_index + 1:]
        #remove some buildings; create parks instead
        num_parks = int(ceil(len(buildings) / (g.const_areas_per_park )))
        parks = []
        for i in xrange(num_parks):
            park_index = rand(0, len(buildings) - 1)
            parks.append(buildings[park_index])
            buildings = buildings[:park_index] + buildings[park_index + 1:]

        #fill the map with ground
        for x,y in alltiles(self.width, self.height):
            self.land[y, x] = ssn_tile('ground')
        #add buildings
        for i in buildings:
            for x,y in i.alltiles():
                if i.point_is_on_edge((x,y)):
                    if not i.point_is_on_corner((x,y)) and rand(1,g.const_building_door_density) == 1:
                        self.land[y, x] = ssn_tile('level door')
                        self.land[y, x].target = 'building'
                    else:
                        self.land[y, x] = ssn_tile('wall')
                elif i.contains_point((x,y)):
                    self.land[y, x] = ssn_tile('rooftop')
        #add parks
        for i in parks:
            for x,y in i.alltiles():
                if i.point_is_on_edge((x,y)):
                    self.land[y, x] = ssn_tile('thin grass')
                elif i.contains_point((x,y)):
                    if rand(1, g.const_park_tree_density) == 1:
                        self.land[y, x] = ssn_tile('tree')
                    elif rand(1, g.const_park_rock_density) == 1:
                        self.land[y, x] = ssn_tile('rock')
                    else:
                        self.land[y, x] = ssn_tile('grass')
        
        self.pc_start_pos = None

    def gen_building(self, g):
        #create empty room that fills the map
        carea = rect(0, 0, self.width, self.height)
        for ctile in carea.alltiles():
            if carea.point_is_on_edge(ctile):
                self.land[ctile[1], ctile[0]] = ssn_tile('wall')
            else:
                self.land[ctile[1], ctile[0]] = ssn_tile('floor')
        
        #shrink the area to just include the interior of the current room
        carea.x += 1
        carea.y += 1
        carea.w -= 2
        carea.h -= 2
        
        def split_room(area, recursion = 0):
            '''split an interior area into 2 rooms'''
            #don't split if recursion limit has been reached
            if recursion >= g.const_room_recursion_limit:
                return
            
            if float(area.w) / float(area.h) > g.const_room_ratio_limit:
                vertical = True
            elif float(area.h) / float(area.w) > g.const_room_ratio_limit:
                vertical = False
            else:
                vertical = (True, False)[rand(0,1)]
            
            door_area = rect(area.x + g.const_room_border, area.y + g.const_room_border, \
                             area.w - (g.const_room_border * 2), area.h - (g.const_room_border * 2))
            door_choices = door_area.alltiles()
            
            #find door choices that would collide with other doors and set them to None, to be removed later
            #ie. prevent this:
            #         ....#..
            #         ####+..
            #         ....#..
            for cindex in xrange(len(door_choices)):
                cdoor = door_choices[cindex]
                if wall_collides_door(cdoor, vertical):
                    door_choices[cindex] = None
            #remove the doors set to None
            while None in door_choices:
                door_choices.remove(None)
            
            #don't split if no door choices remain
            if len(door_choices) <= 0:
                return
                
            #pick a random door, and create the door + wall
            newdoor = door_choices[rand(0,len(door_choices) - 1)]
            create_wall(newdoor, vertical)
            
            #get the 2 new areas, and split them
            if vertical:
                newarea1 = rect(area.x, area.y, newdoor[0] - area.x, area.h) #left
                newarea2 = rect(newdoor[0] + 1, area.y, area.x + area.w - newdoor[0] - 1, area.h) #right
            else: #horizontal
                newarea1 = rect(area.x, area.y, area.w, newdoor[1] - area.y) #top
                newarea2 = rect(area.x, newdoor[1] + 1, area.w, area.y + area.h - newdoor[1] - 1) #bottom
            split_room(newarea1, recursion + 1)
            split_room(newarea2, recursion + 1)
        
        def wall_collides_door(door_loc, vertical):
            '''traces walls out from door_loc,
            and returns True if the wall would ever intersect a door
            the wall stops at other walls'''
            collides = False
            if vertical:
                #expand up
                cy = door_loc[1] - 1
                while self.land[cy, door_loc[0]].type == 'floor':
                    cy -= 1
                    if self.land[cy, door_loc[0]].type == 'door':
                        collides = True
                #expand down
                cy = door_loc[1] + 1
                while self.land[cy, door_loc[0]].type == 'floor':
                    cy += 1
                    if self.land[cy, door_loc[0]].type == 'door':
                        collides = True
            else: #horizontal:
                #expand left
                cx = door_loc[0] - 1
                while self.land[door_loc[1], cx].type == 'floor':
                    cx -= 1
                    if self.land[door_loc[1], cx].type == 'door':
                        collides = True
                #expand right
                cx = door_loc[0] + 1
                while self.land[door_loc[1], cx].type == 'floor':
                    cx += 1
                    if self.land[door_loc[1], cx].type == 'door':
                        collides = True
            return collides
        
        def create_wall(door_loc, vertical):
            '''creates a horizontal/vertical wall,
            expanding from a door at door_loc'''
            self.land[door_loc[1], door_loc[0]] = ssn_tile('door')
            if vertical:
                #expand up
                cy = door_loc[1] - 1
                while self.land[cy, door_loc[0]].type == 'floor':
                    self.land[cy, door_loc[0]] = ssn_tile('wall')
                    cy -= 1
                #expand down
                cy = door_loc[1] + 1
                while self.land[cy, door_loc[0]].type == 'floor':
                    self.land[cy, door_loc[0]] = ssn_tile('wall')
                    cy += 1
            else: #horizontal:
                #expand left
                cx = door_loc[0] - 1
                while self.land[door_loc[1], cx].type == 'floor':
                    self.land[door_loc[1], cx] = ssn_tile('wall')
                    cx -= 1
                #expand right
                cx = door_loc[0] + 1
                while self.land[door_loc[1], cx].type == 'floor':
                    self.land[door_loc[1], cx] = ssn_tile('wall')
                    cx += 1
        
        split_room(carea)
        
        #add some random doors
        door_choices = rect(1, 1, self.width - 2, self.height - 2).alltiles()
        door_count = 0
        while door_count < g.const_room_extra_doors and len(door_choices) > 0:
            cindex = rand(0, len(door_choices) - 1)
            cdoor = door_choices[cindex]
            #check for appropriate door placement:
            #    #              .
            #   .+.    or      #+#
            #    #              .
            #                   
            if (self.land[cdoor[1], cdoor[0]].type == 'wall' \
                and self.land[cdoor[1] + 1, cdoor[0]].type == 'floor' \
                and self.land[cdoor[1] - 1, cdoor[0]].type == 'floor' \
                and self.land[cdoor[1], cdoor[0] + 1].type == 'wall' \
                and self.land[cdoor[1], cdoor[0] - 1].type == 'wall') \
                or ( \
                self.land[cdoor[1], cdoor[0]].type == 'wall' \
                and self.land[cdoor[1] + 1, cdoor[0]].type == 'wall' \
                and self.land[cdoor[1] - 1, cdoor[0]].type == 'wall' \
                and self.land[cdoor[1], cdoor[0] + 1].type == 'floor' \
                and self.land[cdoor[1], cdoor[0] - 1].type == 'floor'):
                self.land[cdoor[1], cdoor[0]] = ssn_tile('door')
                door_count += 1
            door_choices.remove(cdoor)
        
        carea = rect(0, 0, self.width, self.height)
        ext_door_choices = []
        for i in carea.alltiles():
            if carea.point_is_on_edge(i) and not carea.point_is_on_corner(i):
                ext_door_choices.append(i)
        
        while len(ext_door_choices) > 0:
            cindex = rand(0, len(ext_door_choices) - 1)
            cdoor = ext_door_choices[cindex]
            if cdoor[0] == 0: #left
                if self.land[cdoor[1], cdoor[0] + 1].type == 'floor':
                    break
                else:
                    ext_door_choices = ext_door_choices[:cindex] + ext_door_choices[cindex + 1:]
            elif cdoor[0] == self.width - 1: #right
                if self.land[cdoor[1], cdoor[0] - 1].type == 'floor':
                    break
                else:
                    ext_door_choices = ext_door_choices[:cindex] + ext_door_choices[cindex + 1:]
            elif cdoor[1] == 0: #top
                if self.land[cdoor[1] + 1, cdoor[0]].type == 'floor':
                    break
                else:
                    ext_door_choices = ext_door_choices[:cindex] + ext_door_choices[cindex + 1:]
            elif cdoor[1] == self.height - 1: #bottom
                if self.land[cdoor[1] - 1, cdoor[0]].type == 'floor':
                    break
                else:
                    ext_door_choices = ext_door_choices[:cindex] + ext_door_choices[cindex + 1:]

        #set the player starting position to next to the entry door
        if cdoor[0] == 0: #left
            self.pc_start_pos  = (cdoor[0] + 1, cdoor[1])
        elif cdoor[0] == self.width - 1: #right
            self.pc_start_pos  = (cdoor[0] - 1, cdoor[1])
        elif cdoor[1] == 0: #top
            self.pc_start_pos  = (cdoor[0], cdoor[1] + 1)
        elif cdoor[1] == self.height - 1: #bottom
            self.pc_start_pos  = (cdoor[0], cdoor[1] - 1)
        
        #if no doors are left in the list, the last one will be chosen anyway
        
        self.land[cdoor[1], cdoor[0]] = ssn_tile('level door')
        self.land[cdoor[1], cdoor[0]].target = 'city'

class ssn_tile:
    '''a single tile of land
    this is currently used as a simple container for variables'''
    def __init__(self, type):
        self.type = type
        if type == 'empty':
            self.char = 32 #space
            self.back = libtcod.black
            self.fore = libtcod.white
            self.walkable = True
        elif type == 'ground':
            self.char = 32 #space
            self.back = libtcod.Color(152, 118, 84) #light brown
            self.fore = self.back
            self.walkable = True
        elif type == 'rooftop':
            self.char = 32 #space
            self.back = libtcod.light_grey
            self.fore = self.back
            self.walkable = False
        elif type == 'wall':
            self.char = 32 #space
            self.back = libtcod.dark_grey
            self.fore = self.back
            self.walkable = False
        elif type == 'floor':
            self.char = 206 #4-way double line border
            self.back = libtcod.light_grey
            self.fore = libtcod.grey
            self.walkable = True
        elif type == 'grass':
            self.char = 32 #space
            self.back = libtcod.dark_green
            self.fore = self.back
            self.walkable = True
        elif type == 'thin grass':
            self.char = 177 #50% shaded block
            self.back = libtcod.desaturated_green
            self.fore = libtcod.Color(152, 118, 84) #light brown
            self.walkable = True
        elif type == 'tree':
            self.char = 24 #up arrow
            self.back = libtcod.dark_green
            self.fore = libtcod.darker_green
            self.walkable = False
        elif type == 'rock':
            self.char = 42 #star
            self.back = libtcod.dark_green
            self.fore = libtcod.Color(101, 67, 33) #dark brown
            self.walkable = False
        elif type == 'door':
            self.char = 43 #plus
            self.back = libtcod.grey
            self.fore = libtcod.light_grey
            self.walkable = True
        elif type == 'level door':
            self.char = 43 #plus
            self.back = libtcod.dark_grey
            self.fore = libtcod.light_grey
            self.walkable = True
            self.target = ''
        else:
            raise TypeError("Map tile '" + type + "' not found")